package org.springframework.web.servlet.mvc.method;

import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.condition.NameValueExpression;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.util.WebUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * @author float.lu
 */
public abstract class CoRequestMappingInfoHandlerMapping  extends AbstractHandlerMethodMapping<CoRequestMappingInfo> {

    protected CoRequestMappingInfoHandlerMapping() {
        setHandlerMethodMappingNamingStrategy(new CoRequestMappingInfoHandlerMethodMappingNamingStrategy());
    }


    /**
     * Get the URL path patterns associated with this {@link RequestMappingInfo}.
     */
    @Override
    protected Set<String> getMappingPathPatterns(CoRequestMappingInfo info) {
        return info.getPatternsCondition().getPatterns();
    }

    /**
     * Check if the given RequestMappingInfo matches the current request and
     * return a (potentially new) instance with conditions that match the
     * current request -- for example with a subset of URL patterns.
     * @return an info in case of a match; or {@code null} otherwise.
     */
    @Override
    protected CoRequestMappingInfo getMatchingMapping(CoRequestMappingInfo info, HttpServletRequest request) {
        return info.getMatchingCondition(request);
    }

    /**
     * Provide a Comparator to sort RequestMappingInfos matched to a request.
     */
    @Override
    protected Comparator<CoRequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
        return new Comparator<CoRequestMappingInfo>() {
            @Override
            public int compare(CoRequestMappingInfo info1, CoRequestMappingInfo info2) {
                return info1.compareTo(info2, request);
            }
        };
    }

    /**
     * Expose URI template variables, matrix variables, and producible media types in the request.
     * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
     * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
     * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
     */
    @Override
    protected void handleMatch(CoRequestMappingInfo info, String lookupPath, HttpServletRequest request) {
        super.handleMatch(info, lookupPath, request);

        String bestPattern;
        Map<String, String> uriVariables;
        Map<String, String> decodedUriVariables;

        Set<String> patterns = info.getPatternsCondition().getPatterns();
        if (patterns.isEmpty()) {
            bestPattern = lookupPath;
            uriVariables = Collections.emptyMap();
            decodedUriVariables = Collections.emptyMap();
        }
        else {
            bestPattern = patterns.iterator().next();
            uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
            decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
        }

        request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
        if (request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE) == null) {
            request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
        } else {
            decodedUriVariables.putAll((Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
            request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
        }

        if (isMatrixVariableContentAvailable()) {
            Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
            request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
        }

        if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
            Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
            request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
        }
    }

    private boolean isMatrixVariableContentAvailable() {
        return !getUrlPathHelper().shouldRemoveSemicolonContent();
    }

    private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
            HttpServletRequest request, Map<String, String> uriVariables) {

        Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<String, MultiValueMap<String, String>>();
        for (Map.Entry<String, String> uriVar : uriVariables.entrySet()) {
            String uriVarValue = uriVar.getValue();

            int equalsIndex = uriVarValue.indexOf('=');
            if (equalsIndex == -1) {
                continue;
            }

            String matrixVariables;

            int semicolonIndex = uriVarValue.indexOf(';');
            if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) {
                matrixVariables = uriVarValue;
            }
            else {
                matrixVariables = uriVarValue.substring(semicolonIndex + 1);
                uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex));
            }

            MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
            result.put(uriVar.getKey(), getUrlPathHelper().decodeMatrixVariables(request, vars));
        }
        return result;
    }

    /**
     * Iterate all RequestMappingInfos once again, look if any match by URL at
     * least and raise exceptions accordingly.
     * @throws HttpRequestMethodNotSupportedException if there are matches by URL
     * but not by HTTP method
     * @throws HttpMediaTypeNotAcceptableException if there are matches by URL
     * but not by consumable/producible media types
     */
    @Override
    protected HandlerMethod handleNoMatch(Set<CoRequestMappingInfo> requestMappingInfos,
                                          String lookupPath, HttpServletRequest request) throws ServletException {

        Set<String> allowedMethods = new LinkedHashSet<String>(4);

        Set<CoRequestMappingInfo> patternMatches = new HashSet<CoRequestMappingInfo>();
        Set<CoRequestMappingInfo> patternAndMethodMatches = new HashSet<CoRequestMappingInfo>();

        for (CoRequestMappingInfo info : requestMappingInfos) {
            if (info.getPatternsCondition().getMatchingCondition(request) != null) {
                patternMatches.add(info);
                if (info.getMethodsCondition().getMatchingCondition(request) != null) {
                    patternAndMethodMatches.add(info);
                }
                else {
                    for (RequestMethod method : info.getMethodsCondition().getMethods()) {
                        allowedMethods.add(method.name());
                    }
                }
            }
        }

        if (patternMatches.isEmpty()) {
            return null;
        }
        else if (patternAndMethodMatches.isEmpty() && !allowedMethods.isEmpty()) {
            throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
        }

        Set<MediaType> consumableMediaTypes;
        Set<MediaType> producibleMediaTypes;
        List<String[]> paramConditions;

        if (patternAndMethodMatches.isEmpty()) {
            consumableMediaTypes = getConsumableMediaTypes(request, patternMatches);
            producibleMediaTypes = getProducibleMediaTypes(request, patternMatches);
            paramConditions = getRequestParams(request, patternMatches);
        }
        else {
            consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches);
            producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches);
            paramConditions = getRequestParams(request, patternAndMethodMatches);
        }

        if (!consumableMediaTypes.isEmpty()) {
            MediaType contentType = null;
            if (StringUtils.hasLength(request.getContentType())) {
                try {
                    contentType = MediaType.parseMediaType(request.getContentType());
                }
                catch (InvalidMediaTypeException ex) {
                    throw new HttpMediaTypeNotSupportedException(ex.getMessage());
                }
            }
            throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
        }
        else if (!producibleMediaTypes.isEmpty()) {
            throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
        }
        else if (!CollectionUtils.isEmpty(paramConditions)) {
            throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap());
        }
        else {
            return null;
        }
    }

    private Set<MediaType> getConsumableMediaTypes(HttpServletRequest request, Set<CoRequestMappingInfo> partialMatches) {
        Set<MediaType> result = new HashSet<MediaType>();
        for (CoRequestMappingInfo partialMatch : partialMatches) {
            if (partialMatch.getConsumesCondition().getMatchingCondition(request) == null) {
                result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes());
            }
        }
        return result;
    }

    private Set<MediaType> getProducibleMediaTypes(HttpServletRequest request, Set<CoRequestMappingInfo> partialMatches) {
        Set<MediaType> result = new HashSet<MediaType>();
        for (CoRequestMappingInfo partialMatch : partialMatches) {
            if (partialMatch.getProducesCondition().getMatchingCondition(request) == null) {
                result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
            }
        }
        return result;
    }

    private List<String[]> getRequestParams(HttpServletRequest request, Set<CoRequestMappingInfo> partialMatches) {
        List<String[]> result = new ArrayList<String[]>();
        for (CoRequestMappingInfo partialMatch : partialMatches) {
            ParamsRequestCondition condition = partialMatch.getParamsCondition();
            Set<NameValueExpression<String>> expressions = condition.getExpressions();
            if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(request) == null) {
                int i = 0;
                String[] array = new String[expressions.size()];
                for (NameValueExpression<String> expression : expressions) {
                    array[i++] = expression.toString();
                }
                result.add(array);
            }
        }
        return result;
    }

}
